package immutablecollections.functions;

import immutablecollections.*;
import immutablecollections.exceptions.*;
import java.lang.reflect.*;

/**
 * The factory that extracts functions from classes.
 *
 * See the <a href="{@docRoot}/immutablecollections/functions/package-summary.html">package summary</a>
 * for more details of how functions work.
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public class FnFactory
{
    /**
     * The function extracted from {@link Object#toString()}
     */
    public static final Function1<String> toStringFn = FnFactory.on(Object.class).getFn(String.class, "toString");

    /**
     * The function extracted from {@link String#concat(String)}
     */
    public static final Function2<String> concatFn = FnFactory.on(String.class).getFn(String.class, "concat",
            String.class);

    /**
     * The function extracted from {@link Class#getName()}
     */
    static final Function1<String> getNameFn = FnFactory.on(Class.class).getFn(String.class, "getName");

    /**
     * The function extracted from {@link Class#getSimpleName()}
     */
    static final Function1<String> getSimpleNameFn = FnFactory.on(Class.class).getFn(String.class, "getSimpleName");

    //__________________________________________________________________________________________________________________

    final private  Class<?> cls;

    private FnFactory(Class<?> cls)
    {
        this.cls = cls;
    }

    private FnFactory(Object instance)
    {
        this(instance.getClass());
    }

    //__________________________________________________________________________________________________________________

    public <OUT> Function1<OUT> getFn(Class returnType, String name)
    {
        return new Function1Impl<OUT>(getMethod(returnType, name));
    }

    public <OUT> Function2<OUT> getFn(Class returnType, String name, Class arg1Class)
    {
        return new Function2Impl<OUT>(getMethod(returnType, name, arg1Class));
    }

    public <OUT> Function3<OUT> getFn(Class returnType, String name, Class arg1Class, Class arg2Class)
    {
        return new Function3Impl<OUT>(getMethod(returnType, name, arg1Class, arg2Class));
    }

    public <OUT> Function4<OUT> getFn(Class returnType, String name, Class arg1Class, Class arg2Class, Class arg3Class)
    {
        return new Function4Impl<OUT>(getMethod(returnType, name, arg1Class, arg2Class, arg3Class));
    }

    public <OUT> Function5<OUT> getFn(Class returnType, String name, Class arg1Class, Class arg2Class, Class arg3Class,
            Class arg4Class)
    {
        return new Function5Impl<OUT>(getMethod(returnType, name, arg1Class, arg2Class, arg3Class, arg4Class));
    }

    public <OUT> FunctionN<OUT> getFnN(Class returnType, String name, Class... classes)
    {
        return new FunctionNImpl<OUT>(getMethod(returnType, name, classes));
    }

    //__________________________________________________________________________________________________________________

    public <OUT> Function1<OUT> getFnStatic(Class returnType, String name, Class arg1Class)
    {
        return new Function1Impl<OUT>(getMethod(returnType, name, arg1Class));
    }

    public <OUT> Function2<OUT> getFnStatic(Class returnType, String name, Class arg1Class, Class arg2Class)
    {
        return new Function2Impl<OUT>(getMethod(returnType, name, arg1Class, arg2Class));
    }

    public <OUT> Function3<OUT> getFnStatic(Class returnType, String name, Class arg1Class, Class arg2Class,
            Class arg3Class)
    {
        return new Function3Impl<OUT>(getMethod(returnType, name, arg1Class, arg2Class, arg3Class));
    }

    public <OUT> Function4<OUT> getFnStatic(Class returnType, String name, Class arg1Class, Class arg2Class,
            Class arg3Class, Class arg4Class)
    {
        return new Function4Impl<OUT>(getMethod(returnType, name, arg1Class, arg2Class, arg3Class, arg4Class));
    }

    public <OUT> Function5<OUT> getFnStatic(Class returnType, String name, Class arg1Class, Class arg2Class,
            Class arg3Class, Class arg4Class, Class arg5Class)
    {
        return new Function5Impl<OUT>(
                getMethod(returnType, name, arg1Class, arg2Class, arg3Class, arg4Class, arg5Class));
    }

    public <OUT> FunctionN<OUT> getFnNStatic(Class returnType, String name, Class... classes)
    {
        return new FunctionNImpl<OUT>(getMethod(returnType, name, classes));
    }

    //__________________________________________________________________________________________________________________

    private Invoker getMethod(Class<?> returnType, String name, Class... classes)
    {
        try
        {
            Method method = cls.getMethod(name, classes);

            if (!returnType.isAssignableFrom(method.getReturnType()))
            {
                throwNotFound(cls, returnType, name, classes);
            }

            return new SimpleInvoker(method);
        }
        catch (SecurityException e)
        {
            throw new UnexpectedCheckedException(e);
        }
        catch (NoSuchMethodException e)
        {
            throwNotFound(cls, returnType, name, classes);
        }

        // To keep the compiler happy
        throw new InvalidStateException("Should not be here");
    }

    private void throwNotFound(Class<?> type, Class<?> returnType, String name, Class<?>... classes)
    {
        String argTypes = getNameFn == null
                ? "" + ImList.onArray(classes)
                : "" + ImList.onArray(classes).map(getNameFn);

        throw new MethodNotFoundException("No public method called " + name + " with arguments " + argTypes
                + " and a return type of " + returnType.getName() + " can be found on type " + type.getName());
    }

    /**
     * Create a factory on class `cls`
     */
    public static FnFactory on(Class<?> cls)
    {
        return new FnFactory(cls);
    }

    /**
     * Create a factory on class `instance.getClass()`
     */
    public static FnFactory on(Object instance)
    {
        return new FnFactory(instance.getClass());
    }

}
